#include "bookmaker.h"

#include "SkOSFile.h"
#include "SkOSPath.h"

struct CheckEntry {
    string fFile;
    int fLine;
    int fCount;
};

class SpellCheck : public ParserCommon {
public:
    SpellCheck(const BmhParser& bmh) : ParserCommon()
        , fBmhParser(bmh) {
        this->reset();
    }
    bool check(const char* match);
    void report();
private:
    enum class TableState {
        kNone,
        kRow,
        kColumn,
    };

    bool check(Definition* );
    bool checkable(MarkType markType);
    void childCheck(const Definition* def, const char* start);
    void leafCheck(const char* start, const char* end);
    bool parseFromFile(const char* path) override { return true; }
    void printCheck(const string& str);

    void reset() override {
        INHERITED::resetCommon();
        fMethod = nullptr;
        fRoot = nullptr;
        fTableState = TableState::kNone;
        fInCode = false;
        fInConst = false;
        fInDescription = false;
        fInStdOut = false;
    }

    void wordCheck(const string& str);
    void wordCheck(ptrdiff_t len, const char* ch);

    unordered_map<string, CheckEntry> fCode;
    unordered_map<string, CheckEntry> fColons;
    unordered_map<string, CheckEntry> fDigits;
    unordered_map<string, CheckEntry> fDots;
    unordered_map<string, CheckEntry> fParens;  
    unordered_map<string, CheckEntry> fUnderscores;
    unordered_map<string, CheckEntry> fWords;
    const BmhParser& fBmhParser;
    Definition* fMethod;
    RootDefinition* fRoot;
    TableState fTableState;
    bool fInCode;
    bool fInConst;
    bool fInDescription;
    bool fInStdOut;
    typedef ParserCommon INHERITED;
};

void BmhParser::spellCheck(const char* match) const {
    SpellCheck checker(*this);
    checker.check(match);
    checker.report();
}

bool SpellCheck::check(const char* match) {
    for (const auto& topic : fBmhParser.fTopicMap) {
        Definition* topicDef = topic.second;
        if (topicDef->fParent) {
            continue;
        }
        if (!topicDef->isRoot()) {
            return this->reportError<bool>("expected root topic");
        }
        fRoot = topicDef->asRoot();
        if (string::npos == fRoot->fFileName.rfind(match)) {
            continue;
        }
       this->check(topicDef);
    }
    return true;
}

bool SpellCheck::check(Definition* def) {
    fFileName = def->fFileName;
    fLineCount = def->fLineCount;
    string printable = def->printableName();
    const char* textStart = def->fContentStart;
    if (MarkType::kParam != def->fMarkType && MarkType::kConst != def->fMarkType &&
            TableState::kNone != fTableState) {
        fTableState = TableState::kNone;
    }
    switch (def->fMarkType) {
        case MarkType::kAlias:
            break;
        case MarkType::kAnchor:
            break;
        case MarkType::kBug:
            break;
        case MarkType::kClass:
            this->wordCheck(def->fName);
            break;
        case MarkType::kCode:
            fInCode = true;
            break;
        case MarkType::kColumn:
            break;
        case MarkType::kComment:
            break;
        case MarkType::kConst: {
            fInConst = true;
            if (TableState::kNone == fTableState) {
                fTableState = TableState::kRow;
            }
            if (TableState::kRow == fTableState) {
                fTableState = TableState::kColumn;
            }
            this->wordCheck(def->fName);
            const char* lineEnd = strchr(textStart, '\n');
            this->wordCheck(lineEnd - textStart, textStart);
            textStart = lineEnd;
        } break;
        case MarkType::kDefine:
            break;
        case MarkType::kDefinedBy:
            break;
        case MarkType::kDeprecated:
            break;
        case MarkType::kDescription:
            fInDescription = true;
            break;
        case MarkType::kDoxygen:
            break;
        case MarkType::kEnum:
        case MarkType::kEnumClass:
            this->wordCheck(def->fName);
            break;
        case MarkType::kError:
            break;
        case MarkType::kExample:
            break;
        case MarkType::kExternal:
            break;
        case MarkType::kFile:
            break;
        case MarkType::kFormula:
            break;
        case MarkType::kFunction:
            break;
        case MarkType::kHeight:
            break;
        case MarkType::kImage:
            break;
        case MarkType::kLegend:
            break;
        case MarkType::kList:
            break;
        case MarkType::kMember:
            break;
        case MarkType::kMethod: {
            string method_name = def->methodName();
            string formattedStr = def->formatFunction();
            if (!def->isClone()) {
                this->wordCheck(method_name);
            }
            fTableState = TableState::kNone;
            fMethod = def;
            } break;
        case MarkType::kParam: {
            if (TableState::kNone == fTableState) {
                fTableState = TableState::kRow;
            }
            if (TableState::kRow == fTableState) {
                fTableState = TableState::kColumn;
            }
            TextParser paramParser(def->fFileName, def->fStart, def->fContentStart,
                    def->fLineCount);
            paramParser.skipWhiteSpace();
            SkASSERT(paramParser.startsWith("#Param"));
            paramParser.next(); 
            paramParser.skipToNonAlphaNum(); 
            paramParser.skipSpace();
            const char* paramName = paramParser.fChar;
            paramParser.skipToSpace();
            fInCode = true;
            this->wordCheck(paramParser.fChar - paramName, paramName);
            fInCode = false;
       } break;
        case MarkType::kPlatform:
            break;
        case MarkType::kReturn:
            break;
        case MarkType::kRow:
            break;
        case MarkType::kSeeAlso:
            break;
        case MarkType::kStdOut: {
            fInStdOut = true;
            TextParser code(def);
            code.skipSpace();
            while (!code.eof()) {
                const char* end = code.trimmedLineEnd();
                this->wordCheck(end - code.fChar, code.fChar);
                code.skipToLineStart();
            }
            fInStdOut = false;
            } break;
        case MarkType::kStruct:
            fRoot = def->asRoot();
            this->wordCheck(def->fName);
            break;
        case MarkType::kSubtopic:
            this->printCheck(printable);
            break;
        case MarkType::kTable:
            break;
        case MarkType::kTemplate:
            break;
        case MarkType::kText:
            break;
        case MarkType::kTime:
            break;
        case MarkType::kToDo:
            break;
        case MarkType::kTopic:
            this->printCheck(printable);
            break;
        case MarkType::kTrack:

            return true;
        case MarkType::kTypedef:
            break;
        case MarkType::kUnion:
            break;
        case MarkType::kWidth:
            break;
        default:
            SkASSERT(0); 
            break;
    }
    this->childCheck(def, textStart);
    switch (def->fMarkType) {  
        case MarkType::kCode:
            fInCode = false;
            break;
        case MarkType::kColumn:
            break;
        case MarkType::kDescription:
            fInDescription = false;
            break;
        case MarkType::kEnum:
        case MarkType::kEnumClass:
            break;
        case MarkType::kExample:
            break;
        case MarkType::kLegend:
            break;
        case MarkType::kMethod:
            fMethod = nullptr;
            break;
        case MarkType::kConst:
            fInConst = false;
        case MarkType::kParam:
            SkASSERT(TableState::kColumn == fTableState);
            fTableState = TableState::kRow;
            break;
        case MarkType::kReturn:
        case MarkType::kSeeAlso:
            break;
        case MarkType::kRow:
            break;
        case MarkType::kStruct:
            fRoot = fRoot->rootParent();
            break;
        case MarkType::kTable:
            break;
        default:
            break;
    }
    return true;
}

bool SpellCheck::checkable(MarkType markType) {
    return BmhParser::Resolvable::kYes == fBmhParser.fMaps[(int) markType].fResolve;
}

void SpellCheck::childCheck(const Definition* def, const char* start) {
    const char* end;
    fLineCount = def->fLineCount;
    if (def->isRoot()) {
        fRoot = const_cast<RootDefinition*>(def->asRoot());
    }
    for (auto& child : def->fChildren) {
        end = child->fStart;
        if (this->checkable(def->fMarkType)) {
            this->leafCheck(start, end);
        }
        this->check(child);
        start = child->fTerminator;
    }
    if (this->checkable(def->fMarkType)) {
        end = def->fContentEnd;
        this->leafCheck(start, end);
    }
}

void SpellCheck::leafCheck(const char* start, const char* end) {
    TextParser text("", start, end, fLineCount);
    do {
        const char* lineStart = text.fChar;
        text.skipToAlpha();
        if (text.eof()) {
            break;
        }
        const char* wordStart = text.fChar;
        text.fChar = lineStart;
        text.skipTo(wordStart);  
        text.skipToNonAlphaNum();
        fLineCount = text.fLineCount;
        string word(wordStart, text.fChar - wordStart);
        wordCheck(word);
    } while (!text.eof());
}

void SpellCheck::printCheck(const string& str) {
    string word;
    for (std::stringstream stream(str); stream >> word; ) {
        wordCheck(word);
    }
}

void SpellCheck::report() {
    for (auto iter : fWords) {
        if (string::npos != iter.second.fFile.find("undocumented.bmh")) {
            continue;
        }
        if (string::npos != iter.second.fFile.find("markup.bmh")) {
            continue;
        }
        if (string::npos != iter.second.fFile.find("usingBookmaker.bmh")) {
            continue;
        }
        if (iter.second.fCount == 1) {
            SkDebugf("%s %s %d\n", iter.first.c_str(), iter.second.fFile.c_str(),
                    iter.second.fLine);
        }
    }
}

void SpellCheck::wordCheck(const string& str) {
    bool hasColon = false;
    bool hasDot = false;
    bool hasParen = false;
    bool hasUnderscore = false;
    bool sawDash = false;
    bool sawDigit = false;
    bool sawSpecial = false;
    SkASSERT(str.length() > 0);
    SkASSERT(isalpha(str[0]) || '~' == str[0]);
    for (char ch : str) {
        if (isalpha(ch) || '-' == ch) {
            sawDash |= '-' == ch;
            continue;
        }
        bool isColon = ':' == ch;
        hasColon |= isColon;
        bool isDot = '.' == ch;
        hasDot |= isDot;
        bool isParen = '(' == ch || ')' == ch || '~' == ch || '=' == ch || '!' == ch;
        hasParen |= isParen;
        bool isUnderscore = '_' == ch;
        hasUnderscore |= isUnderscore;
        if (isColon || isDot || isUnderscore || isParen) {
            continue;
        }
        if (isdigit(ch)) {
            sawDigit = true;
            continue;
        }
        if ('&' == ch || ',' == ch || ' ' == ch) {
            sawSpecial = true;
            continue;
        }
        SkASSERT(0);
    }
    if (sawSpecial && !hasParen) {
        SkASSERT(0);
    }
    bool inCode = fInCode;
    if (hasUnderscore && isupper(str[0]) && ('S' != str[0] || 'K' != str[1])
            && !hasColon && !hasDot && !hasParen && !fInStdOut && !inCode && !fInConst 
            && !sawDigit && !sawSpecial && !sawDash) {
        std::istringstream ss(str);
        string token;
        while (std::getline(ss, token, '_')) {
            this->wordCheck(token);
        }
        return;
    }
    if (!hasColon && !hasDot && !hasParen && !hasUnderscore 
            && !fInStdOut && !inCode && !fInConst && !sawDigit
            && islower(str[0]) && isupper(str[1])) {
        inCode = true;
    }
    auto& mappy = hasColon ? fColons : 
                  hasDot ? fDots :
                  hasParen ? fParens :
                  hasUnderscore ? fUnderscores :
                  fInStdOut || inCode || fInConst ? fCode :
                  sawDigit ? fDigits : fWords;
    auto iter = mappy.find(str);
    if (mappy.end() != iter) {
        iter->second.fCount += 1;
    } else {
        CheckEntry* entry = &mappy[str];
        entry->fFile = fFileName;
        entry->fLine = fLineCount;
        entry->fCount = 1;
    }
}

void SpellCheck::wordCheck(ptrdiff_t len, const char* ch) {
    leafCheck(ch, ch + len);
}
/*
 * Copyright 2017 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "bookmaker.h"

#include "SkOSFile.h"
#include "SkOSPath.h"

/* 
things to do
if cap word is beginning of sentence, add it to table as lower-case
   word must have only a single initial capital

if word is camel cased, look for :: matches on suffix

when function crosses lines, whole thing isn't seen as a 'word' e.g., search for largeArc in path

words in external not seen
 */
struct CheckEntry {
    string fFile;
    int fLine;
    int fCount;
};

class SpellCheck : public ParserCommon {
public:
    SpellCheck(const BmhParser& bmh) : ParserCommon()
        , fBmhParser(bmh) {
        this->reset();
    }
    bool check(const char* match);
    void report();
private:
    enum class TableState {
        kNone,
        kRow,
        kColumn,
    };

    bool check(Definition* );
    bool checkable(MarkType markType);
    void childCheck(const Definition* def, const char* start);
    void leafCheck(const char* start, const char* end);
    bool parseFromFile(const char* path) override { return true; }
    void printCheck(const string& str);

    void reset() override {
        INHERITED::resetCommon();
        fMethod = nullptr;
        fRoot = nullptr;
        fTableState = TableState::kNone;
        fInCode = false;
        fInConst = false;
        fInDescription = false;
        fInStdOut = false;
    }

    void wordCheck(const string& str);
    void wordCheck(ptrdiff_t len, const char* ch);

    unordered_map<string, CheckEntry> fCode;
    unordered_map<string, CheckEntry> fColons;
    unordered_map<string, CheckEntry> fDigits;
    unordered_map<string, CheckEntry> fDots;
    unordered_map<string, CheckEntry> fParens;  // also hold destructors, operators
    unordered_map<string, CheckEntry> fUnderscores;
    unordered_map<string, CheckEntry> fWords;
    const BmhParser& fBmhParser;
    Definition* fMethod;
    RootDefinition* fRoot;
    TableState fTableState;
    bool fInCode;
    bool fInConst;
    bool fInDescription;
    bool fInStdOut;
    typedef ParserCommon INHERITED;
};

/* This doesn't perform a traditional spell or grammar check, although
   maybe it should. Instead it looks for words used uncommonly and lower
   case words that match capitalized words that are not sentence starters.
   It also looks for articles preceeding capitalized words and their
   modifiers to try to maintain a consistent voice.
   Maybe also look for passive verbs (e.g. 'is') and suggest active ones?
 */
void BmhParser::spellCheck(const char* match) const {
    SpellCheck checker(*this);
    checker.check(match);
    checker.report();
}

bool SpellCheck::check(const char* match) {
    for (const auto& topic : fBmhParser.fTopicMap) {
        Definition* topicDef = topic.second;
        if (topicDef->fParent) {
            continue;
        }
        if (!topicDef->isRoot()) {
            return this->reportError<bool>("expected root topic");
        }
        fRoot = topicDef->asRoot();
        if (string::npos == fRoot->fFileName.rfind(match)) {
            continue;
        }
       this->check(topicDef);
    }
    return true;
}

bool SpellCheck::check(Definition* def) {
    fFileName = def->fFileName;
    fLineCount = def->fLineCount;
    string printable = def->printableName();
    const char* textStart = def->fContentStart;
    if (MarkType::kParam != def->fMarkType && MarkType::kConst != def->fMarkType &&
            TableState::kNone != fTableState) {
        fTableState = TableState::kNone;
    }
    switch (def->fMarkType) {
        case MarkType::kAlias:
            break;
        case MarkType::kAnchor:
            break;
        case MarkType::kBug:
            break;
        case MarkType::kClass:
            this->wordCheck(def->fName);
            break;
        case MarkType::kCode:
            fInCode = true;
            break;
        case MarkType::kColumn:
            break;
        case MarkType::kComment:
            break;
        case MarkType::kConst: {
            fInConst = true;
            if (TableState::kNone == fTableState) {
                fTableState = TableState::kRow;
            }
            if (TableState::kRow == fTableState) {
                fTableState = TableState::kColumn;
            }
            this->wordCheck(def->fName);
            const char* lineEnd = strchr(textStart, '\n');
            this->wordCheck(lineEnd - textStart, textStart);
            textStart = lineEnd;
        } break;
        case MarkType::kDefine:
            break;
        case MarkType::kDefinedBy:
            break;
        case MarkType::kDeprecated:
            break;
        case MarkType::kDescription:
            fInDescription = true;
            break;
        case MarkType::kDoxygen:
            break;
        case MarkType::kEnum:
        case MarkType::kEnumClass:
            this->wordCheck(def->fName);
            break;
        case MarkType::kError:
            break;
        case MarkType::kExample:
            break;
        case MarkType::kExternal:
            break;
        case MarkType::kFile:
            break;
        case MarkType::kFormula:
            break;
        case MarkType::kFunction:
            break;
        case MarkType::kHeight:
            break;
        case MarkType::kImage:
            break;
        case MarkType::kLegend:
            break;
        case MarkType::kList:
            break;
        case MarkType::kMember:
            break;
        case MarkType::kMethod: {
            string method_name = def->methodName();
            string formattedStr = def->formatFunction();
            if (!def->isClone()) {
                this->wordCheck(method_name);
            }
            fTableState = TableState::kNone;
            fMethod = def;
            } break;
        case MarkType::kParam: {
            if (TableState::kNone == fTableState) {
                fTableState = TableState::kRow;
            }
            if (TableState::kRow == fTableState) {
                fTableState = TableState::kColumn;
            }
            TextParser paramParser(def->fFileName, def->fStart, def->fContentStart,
                    def->fLineCount);
            paramParser.skipWhiteSpace();
            SkASSERT(paramParser.startsWith("#Param"));
            paramParser.next(); // skip hash
            paramParser.skipToNonAlphaNum(); // skip Param
            paramParser.skipSpace();
            const char* paramName = paramParser.fChar;
            paramParser.skipToSpace();
            fInCode = true;
            this->wordCheck(paramParser.fChar - paramName, paramName);
            fInCode = false;
       } break;
        case MarkType::kPlatform:
            break;
        case MarkType::kReturn:
            break;
        case MarkType::kRow:
            break;
        case MarkType::kSeeAlso:
            break;
        case MarkType::kStdOut: {
            fInStdOut = true;
            TextParser code(def);
            code.skipSpace();
            while (!code.eof()) {
                const char* end = code.trimmedLineEnd();
                this->wordCheck(end - code.fChar, code.fChar);
                code.skipToLineStart();
            }
            fInStdOut = false;
            } break;
        case MarkType::kStruct:
            fRoot = def->asRoot();
            this->wordCheck(def->fName);
            break;
        case MarkType::kSubtopic:
            this->printCheck(printable);
            break;
        case MarkType::kTable:
            break;
        case MarkType::kTemplate:
            break;
        case MarkType::kText:
            break;
        case MarkType::kTime:
            break;
        case MarkType::kToDo:
            break;
        case MarkType::kTopic:
            this->printCheck(printable);
            break;
        case MarkType::kTrack:
            // don't output children
            return true;
        case MarkType::kTypedef:
            break;
        case MarkType::kUnion:
            break;
        case MarkType::kWidth:
            break;
        default:
            SkASSERT(0); // handle everything
            break;
    }
    this->childCheck(def, textStart);
    switch (def->fMarkType) {  // post child work, at least for tables
        case MarkType::kCode:
            fInCode = false;
            break;
        case MarkType::kColumn:
            break;
        case MarkType::kDescription:
            fInDescription = false;
            break;
        case MarkType::kEnum:
        case MarkType::kEnumClass:
            break;
        case MarkType::kExample:
            break;
        case MarkType::kLegend:
            break;
        case MarkType::kMethod:
            fMethod = nullptr;
            break;
        case MarkType::kConst:
            fInConst = false;
        case MarkType::kParam:
            SkASSERT(TableState::kColumn == fTableState);
            fTableState = TableState::kRow;
            break;
        case MarkType::kReturn:
        case MarkType::kSeeAlso:
            break;
        case MarkType::kRow:
            break;
        case MarkType::kStruct:
            fRoot = fRoot->rootParent();
            break;
        case MarkType::kTable:
            break;
        default:
            break;
    }
    return true;
}

bool SpellCheck::checkable(MarkType markType) {
    return BmhParser::Resolvable::kYes == fBmhParser.fMaps[(int) markType].fResolve;
}

void SpellCheck::childCheck(const Definition* def, const char* start) {
    const char* end;
    fLineCount = def->fLineCount;
    if (def->isRoot()) {
        fRoot = const_cast<RootDefinition*>(def->asRoot());
    }
    for (auto& child : def->fChildren) {
        end = child->fStart;
        if (this->checkable(def->fMarkType)) {
            this->leafCheck(start, end);
        }
        this->check(child);
        start = child->fTerminator;
    }
    if (this->checkable(def->fMarkType)) {
        end = def->fContentEnd;
        this->leafCheck(start, end);
    }
}

void SpellCheck::leafCheck(const char* start, const char* end) {
    TextParser text("", start, end, fLineCount);
    do {
        const char* lineStart = text.fChar;
        text.skipToAlpha();
        if (text.eof()) {
            break;
        }
        const char* wordStart = text.fChar;
        text.fChar = lineStart;
        text.skipTo(wordStart);  // advances line number
        text.skipToNonAlphaNum();
        fLineCount = text.fLineCount;
        string word(wordStart, text.fChar - wordStart);
        wordCheck(word);
    } while (!text.eof());
}

void SpellCheck::printCheck(const string& str) {
    string word;
    for (std::stringstream stream(str); stream >> word; ) {
        wordCheck(word);
    }
}

void SpellCheck::report() {
    for (auto iter : fWords) {
        if (string::npos != iter.second.fFile.find("undocumented.bmh")) {
            continue;
        }
        if (string::npos != iter.second.fFile.find("markup.bmh")) {
            continue;
        }
        if (string::npos != iter.second.fFile.find("usingBookmaker.bmh")) {
            continue;
        }
        if (iter.second.fCount == 1) {
            SkDebugf("%s %s %d\n", iter.first.c_str(), iter.second.fFile.c_str(),
                    iter.second.fLine);
        }
    }
}

void SpellCheck::wordCheck(const string& str) {
    bool hasColon = false;
    bool hasDot = false;
    bool hasParen = false;
    bool hasUnderscore = false;
    bool sawDash = false;
    bool sawDigit = false;
    bool sawSpecial = false;
    SkASSERT(str.length() > 0);
    SkASSERT(isalpha(str[0]) || '~' == str[0]);
    for (char ch : str) {
        if (isalpha(ch) || '-' == ch) {
            sawDash |= '-' == ch;
            continue;
        }
        bool isColon = ':' == ch;
        hasColon |= isColon;
        bool isDot = '.' == ch;
        hasDot |= isDot;
        bool isParen = '(' == ch || ')' == ch || '~' == ch || '=' == ch || '!' == ch;
        hasParen |= isParen;
        bool isUnderscore = '_' == ch;
        hasUnderscore |= isUnderscore;
        if (isColon || isDot || isUnderscore || isParen) {
            continue;
        }
        if (isdigit(ch)) {
            sawDigit = true;
            continue;
        }
        if ('&' == ch || ',' == ch || ' ' == ch) {
            sawSpecial = true;
            continue;
        }
        SkASSERT(0);
    }
    if (sawSpecial && !hasParen) {
        SkASSERT(0);
    }
    bool inCode = fInCode;
    if (hasUnderscore && isupper(str[0]) && ('S' != str[0] || 'K' != str[1])
            && !hasColon && !hasDot && !hasParen && !fInStdOut && !inCode && !fInConst 
            && !sawDigit && !sawSpecial && !sawDash) {
        std::istringstream ss(str);
        string token;
        while (std::getline(ss, token, '_')) {
            this->wordCheck(token);
        }
        return;
    }
    if (!hasColon && !hasDot && !hasParen && !hasUnderscore 
            && !fInStdOut && !inCode && !fInConst && !sawDigit
            && islower(str[0]) && isupper(str[1])) {
        inCode = true;
    }
    auto& mappy = hasColon ? fColons : 
                  hasDot ? fDots :
                  hasParen ? fParens :
                  hasUnderscore ? fUnderscores :
                  fInStdOut || inCode || fInConst ? fCode :
                  sawDigit ? fDigits : fWords;
    auto iter = mappy.find(str);
    if (mappy.end() != iter) {
        iter->second.fCount += 1;
    } else {
        CheckEntry* entry = &mappy[str];
        entry->fFile = fFileName;
        entry->fLine = fLineCount;
        entry->fCount = 1;
    }
}

void SpellCheck::wordCheck(ptrdiff_t len, const char* ch) {
    leafCheck(ch, ch + len);
}
