/*****************************************************************************
 *
 *  describe.cpp
 *
 *      View a changelist a description.
 *
 *****************************************************************************/

#include "sdview.h"

/*****************************************************************************
 *
 *  class CDescribe
 *
 *****************************************************************************/

//
//  The LPARAM of the listview item has the following form:
//
//  HIWORD = enum CATEGORY
//  LOWORD = original index (to break ties during sorting)
//
enum CATEGORY {
    CAT_HEADER,                 // changelist header
    CAT_MATCHED,                // file that matches the pattern
    CAT_BLANK1,                 // separates matched from unmatched
    CAT_UNMATCHED,              // files that don't match the pattern
    CAT_BLANK2,                 // separates unmatched from unchanged
    CAT_UNCHANGED,              // unmatched files that weren't change
};

class CDescribe : public LVFrame, public BGTask {

    friend DWORD CALLBACK CDescribe_ThreadProc(LPVOID lpParameter);

protected:
    LRESULT HandleMessage(UINT uiMsg, WPARAM wParam, LPARAM lParam);

private:

    enum {
        DM_RECALC = WM_APP
    };

    typedef LVFrame super;

    LRESULT ON_WM_CREATE(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_WM_SIZE(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_WM_SETCURSOR(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_WM_COMMAND(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_WM_INITMENU(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_LM_ITEMACTIVATE(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_LM_GETCONTEXTMENU(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_LM_COPYTOCLIPBOARD(UINT uiMsg, WPARAM wParam, LPARAM lParam);
    LRESULT ON_DM_RECALC(UINT uiMsg, WPARAM wParam, LPARAM lParam);

private:                            /* Helpers */
    CDescribe()
    {
        SetAcceleratorTable(MAKEINTRESOURCE(IDA_DESCRIBE));
    }

    void _ResetChildWidth();
    void _AdjustMenu(HMENU hmenu, int iItem, BOOL fContextMenu);
    LPTSTR _GetSanitizedLine(int iItem, LPTSTR pszBuf, UINT cch);
    void ViewOneFile();
    void ViewFileLog();
    int _GetBugNumber(int iItem, BOOL fContextMenu);

    static LPTSTR _SanitizeClipboardText(LPTSTR psz);

    static DWORD CALLBACK s_BGInvoke(LPVOID lpParam);
    DWORD _BGInvoke();

private:
    int         _cxMax;
    int         _iBug;
    Substring   _ssChange;
};

LRESULT CDescribe::ON_WM_CREATE(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    static const LVFCOLUMN s_rgcol[] = {
        { 30 ,IDS_COL_COMMENT   ,LVCFMT_LEFT    },
        {  0 ,0                 ,0              },
    };

    LRESULT lres;

    if (Parse(TEXT("$d"), _pszQuery, &_ssChange)) {
        String str;
        str << TEXT("sdv describe ") << _ssChange;
        SetWindowText(_hwnd, str);

        lres = super::HandleMessage(uiMsg, wParam, lParam);
        if (lres == 0 &&
            SetWindowMenu(MAKEINTRESOURCE(IDM_DESCRIBE)) &&
            CreateChild(LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS |
                        LVS_NOCOLUMNHEADER,
                        LVS_EX_LABELTIP | LVS_EX_FULLROWSELECT) &&
            AddColumns(s_rgcol) &&
            (SetWindowRedraw(_hwndChild, FALSE), TRUE) &&
            BGStartTask(s_BGInvoke, this)) {
        } else {
            lres = -1;
        }
    } else {
        Help(_hwnd, TEXT("#descr"));
        lres = -1;
    }
    return lres;
}

LRESULT CDescribe::ON_WM_SETCURSOR(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    return BGFilterSetCursor(super::HandleMessage(uiMsg, wParam, lParam));
}

LPTSTR CDescribe::_GetSanitizedLine(int iItem, LPTSTR pszBuf, UINT cch)
{
    LPTSTR pszPath = NULL;
    if (iItem >= 0 &&
        ListView_GetItemText(_hwndChild, iItem, pszBuf, cch)) {
        LPTSTR psz =_SanitizeClipboardText(pszBuf);
        if (psz != pszBuf) {
            pszPath = psz;
        }
    }
    return pszPath;
}

void CDescribe::ViewOneFile()
{
    TCHAR sz[MAX_PATH];
    LPTSTR pszPath = _GetSanitizedLine(GetCurSel(), sz, ARRAYSIZE(sz));
    if (pszPath) {
        WindiffOneChange(pszPath);
    }
}

void CDescribe::ViewFileLog()
{
    TCHAR sz[MAX_PATH];
    LPTSTR pszPath = _GetSanitizedLine(GetCurSel(), sz, ARRAYSIZE(sz));
    if (pszPath) {
        String str;
        LPTSTR pszSharp = StrChr(pszPath, TEXT('#'));
        if (pszSharp) {
            *pszSharp++ = TEXT('\0');
            str << TEXT("-#") << pszSharp << TEXT(' ');
        }
        str << pszPath;
        LaunchThreadTask(CFileLog_ThreadProc, str);
    }
}

LRESULT CDescribe::ON_WM_COMMAND(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    int iChange, iBug;

    switch (GET_WM_COMMAND_ID(wParam, lParam)) {
    case IDM_VIEWFILEDIFF:
        ViewOneFile();
        return 0;

    case IDM_VIEWWINDIFF:
        WindiffChangelist(StrToInt(_ssChange._pszMin));
        return 0;

    case IDM_VIEWBUG:
        iBug = _GetBugNumber(GetCurSel(), FALSE);
        if (iBug) {
            OpenBugWindow(_hwnd, iBug);
        }
        break;

    case IDM_VIEWFILELOG:
        ViewFileLog();
        break;

    }
    return super::HandleMessage(uiMsg, wParam, lParam);
}

//
//  Execute the default context menu item.
//
LRESULT CDescribe::ON_LM_ITEMACTIVATE(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    HMENU hmenu = RECAST(HMENU, ON_LM_GETCONTEXTMENU(LM_GETCONTEXTMENU, wParam, 0));
    if (hmenu) {
        FORWARD_WM_COMMAND(_hwnd, GetMenuItemID(hmenu, 0), NULL, 0, SendMessage);
        DestroyMenu(hmenu);
    }
    return 0;
}

int CDescribe::_GetBugNumber(int iItem, BOOL fContextMenu)
{
    LPARAM lParam = RECAST(LPARAM, GetLVItem(iItem));
    int iBug = 0;
    if (HIWORD(lParam) == CAT_HEADER && iItem != 0) {
        iBug = ParseBugNumberFromSubItem(_hwndChild, iItem, 0);
    }

    // If no bug number on the selection, use the default bug number
    // for this changelist.

    if (iBug == 0 && !fContextMenu) {
        iBug = _iBug;
    }

    return iBug;
}

void CDescribe::_AdjustMenu(HMENU hmenu, int iItem, BOOL fContextMenu)
{
    TCHAR sz[MAX_PATH];
    sz[0] = TEXT('\0');
    if (iItem >= 0) {
        ListView_GetItemText(_hwndChild, iItem, sz, ARRAYSIZE(sz));
    }

    //
    //  Disable IDM_VIEWFILEDIFF and IDM_VIEWFILELOG
    //  if this is not a "..." item.
    //
    BOOL fEnable = (Parse(TEXT("... "), sz, NULL) != NULL);
    EnableDisableOrRemoveMenuItem(hmenu, IDM_VIEWFILEDIFF, fEnable, fContextMenu);
    EnableDisableOrRemoveMenuItem(hmenu, IDM_VIEWFILELOG, fEnable, fContextMenu);

    //
    //  If a context menu, then nuke IDM_VIEWWINDIFF if this is not
    //  the "Change" item.
    //

    if (fContextMenu && iItem != 0) {
        DeleteMenu(hmenu, IDM_VIEWWINDIFF, MF_BYCOMMAND);
    }

    AdjustBugMenu(hmenu, _GetBugNumber(iItem, fContextMenu), fContextMenu);

    MakeMenuPretty(hmenu);
}

LRESULT CDescribe::ON_WM_INITMENU(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    _AdjustMenu(RECAST(HMENU, wParam), GetCurSel(), FALSE);
    return 0;
}

LRESULT CDescribe::ON_LM_GETCONTEXTMENU(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    HMENU hmenu = LoadPopupMenu(MAKEINTRESOURCE(IDM_DESCRIBE_POPUP));
    if (hmenu) {
        _AdjustMenu(hmenu, (int)wParam, TRUE);
    }
    return RECAST(LRESULT, hmenu);
}

//
//  If the line begins "...", then strip off everything except for the
//  depot specification.
//
LPTSTR CDescribe::_SanitizeClipboardText(LPTSTR psz)
{
    Substring rgss[2];
    if (Parse(TEXT("... $P#$d"), psz, rgss)) {
        *(rgss[1]._pszMax) = TEXT('\0');
        return rgss[0].Start();
    } else {
        return psz;
    }
}

LRESULT CDescribe::ON_LM_COPYTOCLIPBOARD(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    String str;
    TCHAR sz[MAX_PATH];

    int iMin = (int)wParam;
    int iMax = (int)lParam;

    // If a single-line copy, then special rules apply
    if (iMin + 1 == iMax) {
        if (ListView_GetItemText(_hwndChild, iMin, sz, ARRAYSIZE(sz))) {
            str << _SanitizeClipboardText(sz);
        }
    } else {
        for (int iItem = iMin; iItem < iMax; iItem++) {
            if (ListView_GetItemText(_hwndChild, iItem, sz, ARRAYSIZE(sz))) {
                str << sz;
            }
            str << TEXT("\r\n");
        }
    }
    SetClipboardText(_hwnd, str);
    return 0;
}

LRESULT CDescribe::ON_DM_RECALC(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    ListView_SetColumnWidth(_hwndChild, 0, LVSCW_AUTOSIZE);
    _cxMax = ListView_GetColumnWidth(_hwndChild, 0);
    _ResetChildWidth();

    LVFINDINFO lvfi;
    lvfi.flags = LVFI_PARTIAL;
    lvfi.psz = TEXT("...");

    int iFirst = ListView_FindItem(_hwndChild, -1, &lvfi);
    if (iFirst >= 0) {
        ListView_SetCurSel(_hwndChild, iFirst);
    }

    SetWindowRedraw(_hwndChild, TRUE);

    return 0;
}

void CDescribe::_ResetChildWidth()
{
    RECT rc;
    GetClientRect(_hwndChild, &rc);
    int cxMargins = GetSystemMetrics(SM_CXEDGE) * 2;
    int cxCol = max(_cxMax + cxMargins, rc.right);
    ListView_SetColumnWidth(_hwndChild, 0, cxCol);
}

LRESULT CDescribe::ON_WM_SIZE(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    super::HandleMessage(uiMsg, wParam, lParam);
    if (_cxMax) {
        _ResetChildWidth();
    }
    return 0;
}

LRESULT
CDescribe::HandleMessage(UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uiMsg) {
    FW_MSG(WM_CREATE);
    FW_MSG(WM_SETCURSOR);
    FW_MSG(WM_COMMAND);
    FW_MSG(WM_INITMENU);
    FW_MSG(WM_SIZE);
    FW_MSG(LM_ITEMACTIVATE);
    FW_MSG(LM_GETCONTEXTMENU);
    FW_MSG(LM_COPYTOCLIPBOARD);
    FW_MSG(DM_RECALC);
    }

    return super::HandleMessage(uiMsg, wParam, lParam);
}

//
//  A private helper class that captures the parsing state machine.
//

class DescribeParseState
{
    enum PHASE {
        PHASE_HEADERS,              // collecting the header
        PHASE_FILES,                // collecting the files
        PHASE_DIFFS,                // collecting the diffs
    };

public:
    DescribeParseState(HWND hwndChild, LPCTSTR pszPattern)
        : _m(pszPattern)
        , _hwndChild(hwndChild)
        , _iPhase(PHASE_HEADERS)
        , _iLine(0)
        , _iMatch(-1)
        , _iBug(0)
        , _fAnyMatch(FALSE) { }

    void AddLine(LPTSTR psz, int iCat)
    {
        LVITEM lvi;
        lvi.mask = LVIF_TEXT | LVIF_PARAM;
        lvi.iItem = _iLine;
        lvi.iSubItem = 0;
        lvi.pszText = psz;
        lvi.lParam = MAKELONG(_iLine, iCat);
        _iLine++;

        ChangeTabsToSpaces(psz);

        ListView_InsertItem(_hwndChild, &lvi);
    }

    void SetMatchLine(LPTSTR psz)
    {
        // Turn the "====" into "..." so we can search for it
        LPTSTR pszDots = psz+1;
        pszDots[0] = TEXT('.');
        pszDots[1] = TEXT('.');
        pszDots[2] = TEXT('.');

        LPTSTR pszSharp = StrChr(pszDots, TEXT('#'));
        if (!pszSharp) return;
        pszSharp[1] = TEXT('\0');   // this wipes out the thing after the '#'

        LVFINDINFO lvfi;
        lvfi.flags = LVFI_PARTIAL;
        lvfi.psz = pszDots;

        _iMatch = ListView_FindItem(_hwndChild, 0, &lvfi);

        if (_iMatch >= 0) {
            _cAdded = _cDeleted = 0;
        }
    }

    void FlushMatch()
    {
        if (_iMatch >= 0) {
            String str;
            str.Grow(MAX_PATH-1);
            LVITEM lvi;
            lvi.iItem = _iMatch;
            lvi.mask = LVIF_TEXT | LVIF_PARAM;
            lvi.iSubItem = 0;
            lvi.pszText = str;
            lvi.cchTextMax = str.BufferLength();
            if (ListView_GetItem(_hwndChild, &lvi)) {
                str.SetLength(lstrlen(str));
                str << TEXT(" (") << _cDeleted << TEXT('/') << _cAdded << TEXT(")");
                lvi.pszText = str;
                if (_cDeleted + _cAdded == 0) {
                    lvi.lParam = MAKELONG(LOWORD(lvi.lParam), CAT_UNCHANGED);
                }
                ListView_SetItem(_hwndChild, &lvi);
            }
            _iMatch = -1;
        }
    }

    void ParseLine(String& str)
    {
        Substring rgss[3];

        switch (_iPhase) {
        case PHASE_HEADERS:
            if (Parse(TEXT("... "), str, NULL)) {
                _iPhase = PHASE_FILES;
                goto L_PHASE_FILES;
            }
            if (_iBug == 0 && str[0] == TEXT('\t')) {
                _iBug = ParseBugNumber(str);
            }
            AddLine(str, CAT_HEADER);
            break;

        case PHASE_FILES:
            if (Parse(TEXT("... "), str, NULL)) {
L_PHASE_FILES:
                int iCat;
                if (_m.Matches(str + 4)) {
                    _fAnyMatch = TRUE;
                    iCat = CAT_MATCHED;
                } else {
                    iCat = CAT_UNMATCHED;
                }
                AddLine(str, iCat);
            } else {
                _iPhase = PHASE_DIFFS;
            }
            break;

        case PHASE_DIFFS:
            if (Parse(TEXT("==== "), str, NULL)) {
                SetMatchLine(str);
            } else if (Parse(TEXT("add $d chunks $d lines"), str, rgss)) {
                _cAdded += StrToInt(rgss[1].Finalize());
            } else if (Parse(TEXT("deleted $d chunks $d lines"), str, rgss)) {
                _cDeleted += StrToInt(rgss[1].Finalize());
            } else if (Parse(TEXT("changed $d chunks $d / $d lines"), str, rgss)) {
                _cDeleted += StrToInt(rgss[1].Finalize());
                _cAdded += StrToInt(rgss[2].Finalize());
                FlushMatch();
            }
            break;
        }
    }

    int Finish()
    {
        if (_fAnyMatch) {
            AddLine(TEXT(""), CAT_BLANK1);
        }
        AddLine(TEXT(""), CAT_BLANK2);
        ListView_SortItems(_hwndChild, s_Compare, 0);

        return _iBug;
    }

    static int CALLBACK s_Compare(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
    {
        return (int)(lParam1 - lParam2);
    }

private:
    Match       _m;
    HWND        _hwndChild;
    int         _iPhase;
    int         _iLine;
    int         _iMatch;
    int         _iBug;
    int         _cAdded;
    int         _cDeleted;
    BOOL        _fAnyMatch;
};

DWORD CALLBACK CDescribe::s_BGInvoke(LPVOID lpParam)
{
    CDescribe *self = RECAST(CDescribe *, lpParam);
    return self->_BGInvoke();
}

DWORD CDescribe::_BGInvoke()
{
    DescribeParseState state(_hwndChild, _ssChange._pszMax);

    String str;
    str << TEXT("describe ");
    if (GlobalSettings.IsChurnEnabled()) {
        str << TEXT("-ds ");
    } else {
        str << TEXT("-s ");
    }
    str << _ssChange;

    SDChildProcess proc(str);
    IOBuffer buf(proc.Handle());
    while (buf.NextLine(str)) {
        str.Chomp();
        state.ParseLine(str);
    }

    _iBug = state.Finish();

    PostMessage(_hwnd, DM_RECALC, 0, 0);

    BGEndTask();
    return 0;
}

DWORD CALLBACK CDescribe_ThreadProc(LPVOID lpParameter)
{
    return FrameWindow::RunThread(new CDescribe, lpParameter);
}